﻿using System;
using System.Collections.Generic;
using System.IO;

namespace ReferenceSwap
{
    /// <summary>
    /// Manages changes to references in project and solution files
    /// </summary>
    public class ReferenceManager
    {
        private List<string> excludedExtensions;
        private const string DefaultAssemblyPath = "libs";
        private const string DefaultBuildConfig = "Release";

        public ReferenceManager()
        {
            excludedExtensions = new List<string>();
            excludedExtensions.Add(".snk");
            // any others?
        }

        /// <summary>
        /// Change project references to local assembly references
        /// </summary>
        /// <param name="project">Project to modify</param>
        /// <param name="references">References to change</param>
        /// <param name="assemblyPath">Local path for assemblies</param>
        /// <param name="buildConfiguration">configuration of build assemblies to use</param>
        public void SwapToAssemblyReference(ProjectData project, IEnumerable<ProjectReference> references, string assemblyPath, string buildConfiguration)
        {
            SwapToAssemblyReference(project, references, assemblyPath, buildConfiguration, true);
        }

        /// <summary>
        /// Change project references to local assembly references
        /// </summary>
        /// <param name="project">Project to modify</param>
        /// <param name="references">References to change</param>
        /// <param name="assemblyPath">Local path for assemblies</param>
        /// <param name="buildConfiguration">configuration of build assemblies to use</param>
        /// <param name="withCopy">should copy or not</param>
        public void SwapToAssemblyReference(ProjectData project, IEnumerable<ProjectReference> references, string assemblyPath, string buildConfiguration, bool withCopy)
        {
            Guard.Against<ArgumentNullException>(project == null, "project");
            // maybe we should just use the defaults like the overloads?
            Guard.Against<ArgumentException>(string.IsNullOrEmpty(assemblyPath), "AssemblyPath must have a valid value.");
            Guard.Against<ArgumentException>(string.IsNullOrEmpty(buildConfiguration), "BuildConfiguration must have a valid value.");

            // go to the main project directory and work everything relative to there
            Directory.SetCurrentDirectory(project.ProjectFile.Directory.FullName);

            if (!Directory.Exists(assemblyPath))
                Directory.CreateDirectory(assemblyPath);

            // only need if we are doing the copy
            string buildPath = "";
            if (withCopy)
            {
                buildPath = string.Format("bin/{0}/", buildConfiguration);
                Guard.Against<DirectoryNotFoundException>(!Directory.Exists(buildPath), string.Format("Unable to find requested build directory {0}. Try building the project with the desired configuration.", buildPath));
            }

            foreach (ProjectReference reference in references)
            {
                ProjectData currentProject = new ProjectData(reference.Path);
                // can't reference an executable - don't change anything
                if (currentProject.TypeOfOutput == OutputType.Executable)
                    continue;

                string referenceFile = Path.Combine(assemblyPath, currentProject.OutputFile);

                // need to copy over any content files too...
                HandleContentFiles(currentProject, assemblyPath);

                if (withCopy)
                    File.Copy(Path.Combine(buildPath, currentProject.OutputFile), referenceFile, true);
                else if (!File.Exists(referenceFile))
                    throw new FileNotFoundException("Unable to find assembly in expected location.", referenceFile);

                AssemblyReference assmRef = new AssemblyReference(referenceFile);

                project.RemoveProjectReference(reference);
                project.AddAssemblyReference(assmRef);
            }

            project.Save();
        }
        /// <summary>
        /// Remove project references from a solution
        /// </summary>
        /// <param name="solution">Solution to modify</param>
        /// <param name="references">References to remove</param>
        public void RemoveReferencesFromSolution(SolutionData solution, IEnumerable<ProjectReference> references)
        {
            Guard.Against<ArgumentNullException>(solution == null, "solution");

            foreach (ProjectReference reference in references)
            {
                solution.RemoveProjectReference(reference);
            }

            solution.Save();
        }
        /// <summary>
        /// Move non-system references to a local location
        /// </summary>
        /// <param name="project">Project to modify</param>
        /// <param name="references">References to change</param>
        /// <param name="assemblyPath">Local path for assemblies</param>
        public void MakeAssemblyReferenceLocal(ProjectData project, IEnumerable<AssemblyReference> references, string assemblyPath)
        {
            Guard.Against<ArgumentNullException>(project == null, "project");
            // maybe we should just use the defaults like the overloads?
            Guard.Against<ArgumentException>(string.IsNullOrEmpty(assemblyPath), "AssemblyPath must have a valid value.");

            // go to the main project directory and work everything relative to there
            Directory.SetCurrentDirectory(project.ProjectFile.Directory.FullName);

            if (!Directory.Exists(assemblyPath))
                Directory.CreateDirectory(assemblyPath);

            foreach (AssemblyReference reference in references)
            {
                string referenceFile = Path.Combine(assemblyPath, new FileInfo(reference.Path).Name);
                
                // basically old and new are the same
                if (reference.Path == referenceFile) 
                    continue;

                if (!File.Exists(reference.Path))
                    throw new FileNotFoundException(string.Format("Unable to find Assembly from HintPath. File: {0}", reference.Path));

                // always overwrite                
                File.Copy(reference.Path, referenceFile, true);

                project.ChangeAssemblyReferenceHintPath(reference, referenceFile);
            }

            project.Save();
        }
        /// <summary>
        /// Move over content files as needed for the project
        /// </summary>
        /// <param name="project">project to check for content files</param>
        /// <param name="newPath">path to copy to</param>
        public void MoveContentFiles(ProjectData project, string newPath)
        {
            IEnumerable<ContentFile> filesToCopy = FindContentFilesToCopy(project);

            foreach (ContentFile file in filesToCopy)
            {
                var newFile = new FileInfo(Path.Combine(newPath, file.Name));
                var fileLoc = new FileInfo(Path.Combine(project.ProjectFile.Directory.FullName, file.Path));

                if (!fileLoc.Exists)
                    throw new FileNotFoundException(string.Format("Content file could not be found in expected location: {0}.", fileLoc.FullName));

                if (newFile.Exists)
                {
                    if (file.Copy == FileCopySetting.Always || newFile.LastWriteTime < fileLoc.LastWriteTime)
                        File.Copy(fileLoc.FullName, newFile.FullName, true);
                }
                else
                    File.Copy(fileLoc.FullName, newFile.FullName);

                file.Path = Path.Combine(newPath, newFile.Name);
                project.UpdateFileReference(file);
            }

            project.Save();
        }

        #region Helpers

        private IEnumerable<ContentFile> FindContentFilesToCopy(ProjectData project)
        {
            List<ContentFile> files = new List<ContentFile>();
            foreach (FileReference file in project.Files)
            {
                ContentFile content = file as ContentFile;

                if (ShouldCopyFile(content))
                    files.Add(content);
            }

            return files;
        }

        private bool ShouldCopyFile(ContentFile file)
        {
            if (file == null) 
                return false;

            if(excludedExtensions.Contains(new FileInfo(file.Path).Extension))
                return false;

            if (file.Copy == FileCopySetting.None)
                return false;

            return true;
        }

        private void HandleContentFiles(ProjectData project, string newPath)
        {
            IEnumerable<ContentFile> filesToCopy = FindContentFilesToCopy(project);
            
            foreach (ContentFile file in filesToCopy)
            {
                var newFile = new FileInfo(Path.Combine(newPath, file.Name));
                var fileLoc = new FileInfo(Path.Combine(project.ProjectFile.Directory.FullName, file.Path));

                if (!fileLoc.Exists)
                    throw new FileNotFoundException(string.Format("Content file could not be found in expected location: {0}.", fileLoc.FullName));

                if (newFile.Exists)
                {
                    if (file.Copy == FileCopySetting.Always || newFile.LastWriteTime < fileLoc.LastWriteTime)
                        File.Copy(fileLoc.FullName, newFile.FullName, true);
                }
                else
                    File.Copy(fileLoc.FullName, newFile.FullName);

            }
        }

        #endregion
    }
}
